#include "division_engine/lua/lua_manager.hpp"
#include "lua.h"

#include <cstdlib>
#include <filesystem>
#include <functional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <type_traits>

#include <lua.hpp>

constexpr const char* DIVISION_LUA_ON_DESTROY = "__ON_DESTROY__";
constexpr const char* DIVISION_LUA_ON_UPDATE = "__ON_UPDATE__";

namespace division_engine::lua
{
static void load_and_run_script(const std::filesystem::path& path, lua_State* state);
static void run_event_function(const char* function_name, lua_State* state);

LuaManager::LuaManager() noexcept
  : _state(luaL_newstate())
{
    luaL_openlibs(_state.c_state());
}

LuaManager::~LuaManager()
{
    lua_close(_state.c_state());
}

LuaManager LuaManager::with_search_script_path(const std::filesystem::path& path)
{
    char* lua_path = getenv("LUA_PATH");

    std::filesystem::path abs_path = std::filesystem::absolute(path);
    std::filesystem::path new_path = abs_path / "?.lua";
    std::filesystem::path new_rec_path = abs_path / "?" / "?.lua";

    std::string new_lua_path =
        (lua_path ? std::string { lua_path } + std::string { ";" } : "") +
        std::string { new_path } + ";" + std::string { new_rec_path };

    setenv("LUA_PATH", new_lua_path.c_str(), true);

    return {};
}

void LuaManager::set_watch_file(const std::filesystem::path& main_script_path)
{
    _last_write_time = std::filesystem::last_write_time(main_script_path);
    _main_script_path = main_script_path;

    load_and_run_script(main_script_path, _state.c_state());
}

void LuaManager::update()
{
    if (!_main_script_path.has_value())
    {
        return;
    }

    auto* c_state = _state.c_state();
    const auto script_path = _main_script_path.value();

    if (check_script_changes(script_path))
    {
        run_event_function(DIVISION_LUA_ON_DESTROY, c_state);
        load_and_run_script(script_path, c_state);
    }
    else
    {
        run_event_function(DIVISION_LUA_ON_UPDATE, c_state);
    }
}

bool LuaManager::check_script_changes(const std::filesystem::path& script_path)
{
    if (!std::filesystem::exists(script_path))
    {
        return false;
    }

    const auto& last_write_time = std::filesystem::last_write_time(script_path);
    if (last_write_time == _last_write_time)
    {
        return false;
    }

    _last_write_time = last_write_time;
    return true;
}

static void load_and_run_script(const std::filesystem::path& path, lua_State* c_state)
{
    if (!std::filesystem::exists(path))
    {
        throw std::runtime_error { "There is no the lua script by path: " +
                                   std::string { path } };
    }

    const int error = luaL_loadfile(c_state, path.c_str()) || lua_pcall(c_state, 0, 0, 0);
    if (error)
    {
        std::string error_message {
            "There is an error when load lua script by path: " + std::string { path } +
            ".\nError message: " + lua_tostring(c_state, -1)
        };

        lua_pop(c_state, 1);

        throw std::runtime_error { error_message };
    }
}

static void run_event_function(const char* function_name, lua_State* c_state)
{
    lua_getglobal(c_state, function_name);
    if (lua_isfunction(c_state, -1) && lua_pcall(c_state, 0, 0, 0))
    {
        const std::string error_message = lua_tostring(c_state, -1);
        lua_pop(c_state, 1);

        throw std::runtime_error { error_message };
    }
}
}